﻿using System;
using System.Collections;
using System.Collections.Generic;
using NRank.Common;

namespace NRank.Algorithms
{
	///<summary>
	/// Implementation of NestedLoopRankJoin algorithm
	///</summary>
	///<typeparam name="T1">Type of left source items</typeparam>
	///<typeparam name="T2">Type of right source items</typeparam>
	///<typeparam name="T">Type of output items</typeparam>
	public class NestedLoopRankJoin<T1, T2, T> : IEnumerable<IRanked<T>>, IDepthAwareContainer
	{
		/// <summary>
		/// Join function
		/// </summary>
		private readonly Func<T1, T2, T> joinFunc;

		/// <summary>
		/// Aggregate function
		/// </summary>
		private readonly Func<double, double, double> aggregateFunction;

		/// <summary>
		/// Join condition
		/// </summary>
		private readonly Func<T1, T2, bool> joinCondition;

		/// <summary>
		/// Left source
		/// </summary>
		//private readonly IEnumerable<IRanked<T1>> leftSource;
		private readonly DepthAware<IRanked<T1>> leftSource;

		/// <summary>
		/// Right source
		/// </summary>
		private readonly DepthAware<IRanked<T2>> rightSource;

		/// <summary>
		/// Create instance of nested loop rank join
		/// </summary>
		/// <param name="leftSource">Left source</param>
		/// <param name="rightSource">Right source</param>
		/// <param name="rankFunc1">Left ranking function</param>
		/// <param name="rankFunc2">Right ranking function</param>
		/// <param name="joinFunc">Join function</param>
		/// <param name="aggregateFunction">Aggregate function</param>
		/// <param name="joinCondition"></param>
		public NestedLoopRankJoin(
			IEnumerable<T1> leftSource,
			IEnumerable<T2> rightSource,
			Func<T1, double> rankFunc1,
			Func<T2, double> rankFunc2,
			Func<T1, T2, T> joinFunc,
			Func<double, double, double> aggregateFunction = null,
			Func<T1, T2, bool> joinCondition = null)
		{
			if (leftSource == null) throw new ArgumentNullException("leftSource");
			if (rightSource == null) throw new ArgumentNullException("rightSource");
			if (rankFunc1 == null) throw new ArgumentNullException("rankFunc1");
			if (joinFunc == null) throw new ArgumentNullException("joinFunc");

			this.joinFunc = joinFunc;
			this.aggregateFunction = aggregateFunction ?? Defaults.DefaultAggregateFunction;
			this.joinCondition = joinCondition;

			this.leftSource = new DepthAware<IRanked<T1>>(leftSource.AsRanked(rankFunc1));
			this.rightSource = new DepthAware<IRanked<T2>>(rightSource.AsRanked(rankFunc2));
		}

		/// <summary>
		/// Current threshold
		/// </summary>
		public double Threshold { get; private set; }

		/// <summary>
		/// Algorithm implementation
		/// </summary>
		/// <returns>Rank-joined items</returns>
		private IEnumerable<IRanked<T>> GetItems()
		{
			Func<IRanked<T1>, IRanked<T2>, IRanked<T>> joinFuncWithRank = (i1, i2) => joinFunc(i1.Object, i2.Object).AsRanked(aggregateFunction(i1.Rank, i2.Rank));

			var joinedResults = new Heap<IRanked<T>>();

			var left = this.leftSource.GetEnumerator();

			var leftBoundHolder = new BoundHolder();
			
			var rightBoundHolder = new BoundHolder {MaxRank = double.MinValue, MinRank = double.MaxValue};
			var rightAdapter = this.rightSource.AsCached();

			foreach (var item in rightAdapter)
			{
				rightBoundHolder.MinRank = Math.Min(rightBoundHolder.MinRank, item.Rank);
				rightBoundHolder.MaxRank = Math.Max(rightBoundHolder.MaxRank, item.Rank);
			}

			var thresholdCounter = new ThresholdCounter(leftBoundHolder, rightBoundHolder, this.aggregateFunction);

			while (left.MoveNext())
			{
				var leftItem = left.Current;

				leftBoundHolder.MinRank = leftItem.Rank;
				leftBoundHolder.MaxRank = leftItem.Rank;

				// update threshold
				Threshold = thresholdCounter.CountThreshold();

				foreach (var rightSeenItem in rightAdapter)
				{
					if (joinCondition == null || joinCondition(leftItem.Object, rightSeenItem.Object))
					{
						joinedResults.Add(joinFuncWithRank(leftItem, rightSeenItem));
					}
				}

				while (joinedResults.Count > 0 && joinedResults.Peek().Rank >= Threshold)
				{
					yield return joinedResults.RemoveTop();
				}
			}

			// all is read, just yield the rest
			while (joinedResults.Count > 0)
			{
				yield return joinedResults.RemoveTop();
			}
		}

		/// <summary>
		/// Enumerator implementation
		/// </summary>
		/// <returns>Result enumerator</returns>
		public IEnumerator<IRanked<T>> GetEnumerator()
		{
			return GetItems().GetEnumerator();
		}

		/// <summary>
		/// Non-generic enumerator implementation
		/// </summary>
		/// <returns>Non-generic result enumerator</returns>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		public int LeftDepth
		{
			get { return leftSource.Depth; }
		}

		public int RightDepth
		{
			get { return rightSource.Depth; }
		}
	}
}